package dv.apkeep;

import dv.network.Change;
import dv.network.Network;
import dv.network.Port;
import jdd.bdd.BDD;

import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;

public class Model {
    private Network network;
    public HashMap<Port, HashSet<Integer>> portToPreds;
    public HashMap<Integer, HashSet<Port>> predToPorts;
    public BDDEngine bddEngine;
    private HashSet<Integer> transferredPred; // init when update

    public Model(Network network) {
        this.network = network;
        this.portToPreds = new HashMap<>();
        this.predToPorts = new HashMap<>();

        // add initial pred for default port
        this.predToPorts.put(1, new HashSet<Port>());
        this.bddEngine = new BDDEngine(128); // for ipv6
    }

    public HashSet<Integer> update(HashSet<Change> changes) {
        this.transferredPred = new HashSet<>();
        BDD bdd = this.bddEngine.bdd;
        for (Change c : changes) {
            this.portToPreds.computeIfAbsent(c.getOldPort(), k -> new HashSet<>());
            this.portToPreds.computeIfAbsent(c.getNewPort(), k -> new HashSet<>());
            int delta = c.getHit();
            HashSet<Integer> oldPortPreds = (HashSet<Integer>) this.portToPreds.get(c.getOldPort()).clone();
            for (int p : oldPortPreds) {
                int tmp = bddEngine.bdd.ref(bddEngine.bdd.and(p, delta));
                if (tmp != BDDEngine.BDDFalse) {
                    if (tmp != p) {
                        int tmp2 = bdd.ref(bdd.not(c.getHit()));
                        int tmp3 = bdd.ref(bdd.and(p, tmp2));
                        bdd.deref(tmp2);
                        this.split(p, tmp, tmp3);
//                        bdd.deref(tmp3);
                    }
                    this.transfer(tmp, c.getOldPort(), c.getNewPort());

                    // do merge
                    HashMap<Integer, HashSet<Port>> predToPorts = (HashMap<Integer, HashSet<Port>>) this.predToPorts.clone();
                    HashSet<Integer> oldPreds = (HashSet<Integer>) this.portToPreds.get(c.getNewPort()).clone();
                    for (int pp : oldPreds) {
                        if (pp != tmp && predToPorts.get(pp).equals(predToPorts.get(tmp))) {
                            int tmp4 = bdd.ref(bdd.or(tmp, pp));
                            this.merge(tmp, pp, tmp4);
                        }
                    }

                    int tmp5 = bdd.ref(bdd.not(p));
                    int newdelta = bdd.ref(bdd.and(delta, tmp5));
                    bdd.deref(delta);
                    delta = newdelta;
                    bdd.deref(tmp5);
                }
            }
            bdd.deref(delta);
        }
        return this.transferredPred;
    }

    private void split(int p, int p1, int p2) {
        for (Port port : this.predToPorts.get(p)) {
            this.portToPreds.get(port).addAll(Arrays.asList(p1, p2));
            this.portToPreds.get(port).remove(p);
        }
        this.predToPorts.put(p1, (HashSet<Port>) this.predToPorts.get(p).clone());
        this.predToPorts.put(p2, (HashSet<Port>) this.predToPorts.get(p).clone());
        this.predToPorts.remove(p);
        // here we derefer p
        this.bddEngine.bdd.deref(p);

        if (this.transferredPred.contains(p)) {
            this.transferredPred.addAll(Arrays.asList(p1, p2));
            this.transferredPred.remove(p);
        }
    }

    private void transfer(int p, Port from, Port to) {
        this.portToPreds.get(from).remove(p);
        this.portToPreds.get(to).add(p);
        this.predToPorts.get(p).add(to);
        this.predToPorts.get(p).remove(from);
        this.transferredPred.add(p);
    }

    private void merge(int p1, int p2, int p) {
        for (Port port : this.predToPorts.get(p1)) {
            this.portToPreds.get(port).add(p);
            this.portToPreds.get(port).removeAll(Arrays.asList(p1, p2));
        }
        this.predToPorts.put(p, (HashSet<Port>) this.predToPorts.get(p1).clone());
        this.predToPorts.remove(p1);
        this.predToPorts.remove(p2);

        if (this.transferredPred.contains(p1) || this.transferredPred.contains(p2)) {
            this.transferredPred.add(p);
            this.transferredPred.removeAll(Arrays.asList(p1, p2));
        }
    }
}

